package cn.cuilan.httpfileserver;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
import java.util.regex.Pattern;

/**
 * HTTP文件服务器处理类
 *
 * @author zhang.yan
 * @date 2020/3/21
 */
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    /**
     * 地址
     */
    private final String url;

    /**
     * 是否显示隐藏文件
     */
    private boolean showHiddenFile = false;

    /**
     * 是否显示非法字符
     */
    private boolean showInvaildSymbol = false;

    /**
     * 不安全的URI
     */
    private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");

    /**
     * 正则定义允许的文件名称
     */
    private static final Pattern ALLOWED_FILE_NAME = Pattern
            .compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");

    public HttpFileServerHandler(String url, boolean showHiddenFile, boolean showInvaildSymbol) {
        this.url = url;
        this.showHiddenFile = showHiddenFile;
        this.showInvaildSymbol = showInvaildSymbol;
    }

    @Override
    protected void messageReceived(ChannelHandlerContext channelHandlerContext, FullHttpRequest request) throws Exception {
        // 解码失败
        if (!request.getDecoderResult().isSuccess()) {
            sendError(channelHandlerContext, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        // 非GET请求都不允许访问
        if (request.getMethod() != HttpMethod.GET) {
            sendError(channelHandlerContext, HttpResponseStatus.METHOD_NOT_ALLOWED);
            return;
        }
        final String uri = request.getUri();
        final String path = sanitizeUri(uri);
        // 禁止访问
        if (path == null) {
            sendError(channelHandlerContext, HttpResponseStatus.FORBIDDEN);
            return;
        }
        File file = new File(path);
        // 是否显示隐藏文件
        if (!showHiddenFile && file.isHidden()) {
            sendError(channelHandlerContext, HttpResponseStatus.NOT_FOUND);
            return;
        }
        // 文件不存在
        if (!file.exists()) {
            sendError(channelHandlerContext, HttpResponseStatus.NOT_FOUND);
            return;
        }
        // 如果是目录
        if (file.isDirectory()) {
            if (uri.endsWith("/")) {
                sendListing(channelHandlerContext, file);
            } else {
                sendRedirect(channelHandlerContext, uri + '/');
            }
            return;
        }
        // 不是文件
        if (!file.isFile()) {
            sendError(channelHandlerContext, HttpResponseStatus.FORBIDDEN);
            return;
        }
        RandomAccessFile randomAccessFile = null;
        try {
            // 以只读方式打开文件
            randomAccessFile = new RandomAccessFile(file, "r");
        } catch (FileNotFoundException e) {
            sendError(channelHandlerContext, HttpResponseStatus.NOT_FOUND);
            return;
        }

        long fileLength = randomAccessFile.length();
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        // 设置响应消息体的长度
        HttpHeaders.setContentLength(response, fileLength);
        // 设置响应文档类型
        setContentTypeHeader(response, file);
        // 判断是否保持连接
        if (HttpHeaders.isKeepAlive(request)) {
            response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
        }
        channelHandlerContext.write(response);
        ChannelFuture sendFileFuture = channelHandlerContext.write(new ChunkedFile(randomAccessFile, 0,
                fileLength, 8192), channelHandlerContext.newProgressivePromise());
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) throws Exception {
                // total unknown
                if (total < 0) {
                    System.err.println("传输中: " + progress);
                } else {
                    System.err.println("传输中: " + progress + " / " + total);
                }
            }

            @Override
            public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) throws Exception {
                System.out.println("文件传输完成...");
            }
        });
        ChannelFuture lastContentFuture = channelHandlerContext.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        if (!HttpHeaders.isKeepAlive(request)) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        if (ctx.channel().isActive()) {
            // 500服务器内部错误
            sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 校验并格式化请求URI
     *
     * @param uri 请求URI
     * @return 返回验证后的格式化URI
     */
    private String sanitizeUri(String uri) {
        try {
            uri = URLDecoder.decode(uri, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            try {
                uri = URLDecoder.decode(uri, "iso-8859-1");
            } catch (UnsupportedEncodingException ex) {
                ex.printStackTrace();
                throw new Error();
            }
        }
        if (!uri.startsWith("/")) {
            return null;
        }
        uri = uri.replace('/', File.separatorChar);
        if (uri.contains(File.separator + '.')
                || uri.contains('.' + File.separator)
                || uri.startsWith(".")
                || uri.endsWith(".")) {
            return null;
        }
        return url + File.separator + uri;
    }

    /**
     * 发送HTML文件列表
     *
     * @param ctx ChannelHandlerContext对象
     * @param dir 目录
     */
    private void sendListing(ChannelHandlerContext ctx, File dir) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=UTF-8");
        StringBuilder buf = new StringBuilder();
        SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm");
        String dirPath = dir.getPath();
        buf.append("<!DOCTYPE html>\r\n");
        buf.append("<html><head><title>目录</title>\r\n");
        buf.append("<style>");
        buf.append("body{margin:20px;}");
        buf.append("ul{list-style-type:none;}");
        buf.append(".header{font-weight:bold;}");
        buf.append("pre{font-family: monospace;white-space: pre;}");
        buf.append("a:-webkit-any-link{color:-webkit-link;cursor:pointer;text-decoration:none;}");
        buf.append("</style>");
        buf.append("</head><body>\r\n");
        buf.append("<h2>");
        buf.append("目录：").append(dirPath);
        buf.append("</h2><br>\r\n");
        buf.append("<pre>");
        buf.append("<span class=\"header\">文件名称                                    修改时间                文件大小</span><hr>");
        buf.append("<a href=\"../\">上一级</a>\r\n");
        for (File f : Objects.requireNonNull(dir.listFiles())) {
            if (f.isHidden() || !f.canRead()) {
                continue;
            }
            String name = f.getName();
            long lastModified = f.lastModified();
            long length = f.length();
            // 过滤非法字符
            if (!showInvaildSymbol) {
                if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
                    continue;
                }
            }
            buf.append("<a href=\"");
            buf.append(name);
            buf.append("\">");
            // 处理名称长度
            buf.append(formatName(name));
            buf.append("</a>");
            buf.append(format.format(new Date(lastModified))).append("            ");
            if (f.isFile()) {
                buf.append(formatSize(length));
            }
            buf.append("\r\n");
        }
        buf.append("</pre></body></html>\r\n");
        ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
        response.content().writeBytes(buffer);
        buffer.release();
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    public static String formatSize(long size) {
        if (size < 1024) {
            return size + "B";
        }
        long kSize = size / 1024L;
        return kSize + "KB";
    }

    public static String formatName(String fileName) {
        int length = fileName.length();
        StringBuilder sb = new StringBuilder();
        if (length < 48) {
            sb.append(fileName);
            for (int i = 0; i < 48 - length; i++) {
                sb.append(" ");
            }
        } else if (length == 48) {
            return fileName;
        } else {
            sb.append(fileName, 0, 10);
            sb.append("...");
            sb.append(fileName, length - 10, length);
        }
        return sb.toString();
    }

    /**
     * 设置重定向
     *
     * @param ctx    ChannelHandlerContext
     * @param newUri 重定向地址
     */
    private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
        response.headers().set(HttpHeaders.Names.LOCATION, newUri);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 返回HTTP错误状态信息
     *
     * @param ctx    ChannelHandlerContext
     * @param status HttpResponseStatus
     */
    private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
                Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
        response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 设置响应文档类型
     *
     * @param response HttpResponse响应对象
     * @param file     响应文件对象
     */
    private static void setContentTypeHeader(HttpResponse response, File file) {
        MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
        response.headers().set(HttpHeaders.Names.CONTENT_TYPE,
                mimeTypesMap.getContentType(file.getPath()));
    }
}
